/*
 * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package org.eclipse.imagen.media.rmi;

import java.awt.*;
import java.awt.geom.Rectangle2D;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.renderable.ContextualRenderedImageFactory;
import java.awt.image.renderable.ParameterBlock;
import java.awt.image.renderable.RenderContext;
import java.awt.image.renderable.RenderableImage;
import java.io.ByteArrayOutputStream;
import java.io.Serializable;
import java.net.InetAddress;
import java.rmi.Naming;
import java.rmi.RMISecurityManager;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;
import org.eclipse.imagen.*;
import org.eclipse.imagen.media.remote.JAIServerConfigurationSpi;
import org.eclipse.imagen.media.serialize.SerializableState;
import org.eclipse.imagen.media.serialize.SerializerFactory;
import org.eclipse.imagen.media.tilecodec.TileCodecUtils;
import org.eclipse.imagen.media.util.ImageUtil;
import org.eclipse.imagen.media.util.Service;
import org.eclipse.imagen.registry.CRIFRegistry;
import org.eclipse.imagen.remote.*;
import org.eclipse.imagen.tilecodec.TileCodecDescriptor;
import org.eclipse.imagen.tilecodec.TileCodecParameterList;
import org.eclipse.imagen.tilecodec.TileEncoder;
import org.eclipse.imagen.tilecodec.TileEncoderFactory;
import org.eclipse.imagen.util.ImagingListener;

/**
 * The server-side implementation of the ImageServer interface. A JAIRMIImageServer has a RenderedImage source, acquired
 * via one of three setSource() methods. The first takes a RenderedImage directly as its parameter; this image is simply
 * copied over the network using the normal RMI mechanisms. Note that not every image can be transferred in this way --
 * for example, attempting to pass an OpImage that uses native code or that depends on the availability of a class not
 * resident on the server as a parameter will cause an exception to be thrown.
 *
 * <p>The second and third ways of setting sources make use of the RenderedOp and RenderableOp classes to send a
 * high-level description of an image chain based on operation names. This chain will be copied over to the server using
 * RMI, where it will be expanded into an OpImage chain using the server's registry. This is the preferred method since
 * it requires less data transfer and offers a better chance of success. It may still fail if the sources or parameters
 * of any operation in the chain are not serializable.
 *
 * <p>RMI requires all remote methods to declare `throws RemoteException' in their signatures. It is up to the client to
 * deal with errors. A simple implementation of error handling may be found in the RemoteRenderedImage class.
 *
 * <p>This class contains a main() method that should be run on the server after starting the RMI registry. The registry
 * will then construct new instances of JAIRMIImageServer on demand.
 *
 * @see ImageServer
 * @see RenderedOp
 * @since 1.1
 */
public class JAIRMIImageServer extends UnicastRemoteObject implements ImageServer {

    private boolean DEBUG = true;

    /** Tag to represent a null property. */
    public static final Object NULL_PROPERTY = RMIImageImpl.NULL_PROPERTY;

    /** Identifier counter for the remote images. */
    private static long idCounter = 0;

    /**
     * The RenderedImage nodes hashed by an ID string which must be unique across all possible clients of this object.
     */
    private static Hashtable nodes = new Hashtable();

    /** Hashtable to store the negotiated values for each id. */
    private static Hashtable negotiated = new Hashtable();

    /** Hashtable to store the number of references existing to a particular id on this server. */
    private static Hashtable refCount = new Hashtable();

    /**
     * Retrieve a PlanarImage source from the Hashtable of sources.
     *
     * @param id The unique ID of the source.
     * @return The source.
     */
    private static PlanarImage getSource(Long id) throws RemoteException {
        Object obj = null;
        if (nodes == null || (obj = nodes.get(id)) == null) {
            throw new RemoteException(JaiI18N.getString("RMIImageImpl2"));
        }
        return (PlanarImage) obj;
    }

    /**
     * Retrieve a PropertySource from the Hashtable of PropertySources.
     *
     * @param id The unique ID of the source.
     * @return The PropertySource.
     */
    private static PropertySource getPropertySource(Long id) throws RemoteException {

        Object obj = nodes.get(id);
        return (PropertySource) obj;
    }

    /** Constructs a JAIRMIImageServer with a source to be specified later. */
    public JAIRMIImageServer(int serverport) throws RemoteException {
        super(serverport);
    }

    /**
     * Returns the identifier of the remote image. This method should be called to return an identifier before any other
     * methods are invoked. The same ID must be used in all subsequent references to the remote image.
     */
    public synchronized Long getRemoteID() throws RemoteException {
        return new Long(++idCounter);
    }

    /** Disposes of any resouces allocated to the client object with the specified ID. */
    public synchronized void dispose(Long id) throws RemoteException {

        int count = ((Integer) refCount.get(id)).intValue();

        if (count == 1) {

            // If this was the last reference, remove all Objects
            // associated with this id in various Hashtables.
            if (nodes != null) {
                nodes.remove(id);
                negotiated.remove(id);
            }

            refCount.remove(id);

        } else {

            // Decrement count of references to this id.
            count--;
            if (count == 0) {
                refCount.remove(id);
            }
            refCount.put(id, new Integer(count));
        }
    }

    /**
     * Increments the reference count for this id, i.e. increments the number of RMIServerProxy objects that currently
     * reference this id.
     */
    public void incrementRefCount(Long id) throws RemoteException {
        Integer iCount = (Integer) refCount.get(id);
        int count = 0;
        if (iCount != null) {
            count = iCount.intValue();
        }
        count++;
        refCount.put(id, new Integer(count));
    }

    /**
     * Gets a property from the property set of this image. If the property is undefined the constant NULL_PROPERTY is
     * returned.
     */
    public Object getProperty(Long id, String name) throws RemoteException {

        PropertySource ps = getPropertySource(id);
        Object property = ps.getProperty(name);

        if (property == null || property.equals(java.awt.Image.UndefinedProperty)) {
            property = NULL_PROPERTY;
        }

        return property;
    }

    /**
     * Returns a list of names recognized by getProperty().
     *
     * @return an array of Strings representing property names.
     */
    public String[] getPropertyNames(Long id) throws RemoteException {

        PropertySource ps = getPropertySource(id);
        return ps.getPropertyNames();
    }

    /**
     * Returns a list of names recognized by getProperty().
     *
     * @return an array of Strings representing property names.
     */
    public String[] getPropertyNames(String opName) throws RemoteException {

        return (CRIFRegistry.get(null, opName)).getPropertyNames();
    }

    /** Returns the minimum X coordinate of the ImageServer. */
    public int getMinX(Long id) throws RemoteException {
        return getSource(id).getMinX();
    }

    /** Returns the smallest X coordinate to the right of the ImageServer. */
    public int getMaxX(Long id) throws RemoteException {

        return getSource(id).getMaxX();
    }

    /** Returns the minimum Y coordinate of the ImageServer. */
    public int getMinY(Long id) throws RemoteException {

        return getSource(id).getMinY();
    }

    /** Returns the smallest Y coordinate below the ImageServer. */
    public int getMaxY(Long id) throws RemoteException {

        return getSource(id).getMaxY();
    }

    /** Returns the width of the ImageServer. */
    public int getWidth(Long id) throws RemoteException {

        return getSource(id).getWidth();
    }

    /** Returns the height of the ImageServer. */
    public int getHeight(Long id) throws RemoteException {

        return getSource(id).getHeight();
    }

    /** Returns the width of a tile in pixels. */
    public int getTileWidth(Long id) throws RemoteException {

        return getSource(id).getTileWidth();
    }

    /** Returns the height of a tile in pixels. */
    public int getTileHeight(Long id) throws RemoteException {

        return getSource(id).getTileHeight();
    }

    /** Returns the X coordinate of the upper-left pixel of tile (0, 0). */
    public int getTileGridXOffset(Long id) throws RemoteException {

        return getSource(id).getTileGridXOffset();
    }

    /** Returns the Y coordinate of the upper-left pixel of tile (0, 0). */
    public int getTileGridYOffset(Long id) throws RemoteException {

        return getSource(id).getTileGridYOffset();
    }

    /** Returns the index of the leftmost column of tiles. */
    public int getMinTileX(Long id) throws RemoteException {

        return getSource(id).getMinTileX();
    }

    /** Returns the number of tiles along the tile grid in the horizontal direction. */
    public int getNumXTiles(Long id) throws RemoteException {

        return getSource(id).getNumXTiles();
    }

    /** Returns the index of the uppermost row of tiles. */
    public int getMinTileY(Long id) throws RemoteException {

        return getSource(id).getMinTileY();
    }

    /** Returns the number of tiles along the tile grid in the vertical direction. */
    public int getNumYTiles(Long id) throws RemoteException {

        return getSource(id).getNumYTiles();
    }

    /** Returns the index of the rightmost column of tiles. */
    public int getMaxTileX(Long id) throws RemoteException {

        return getSource(id).getMaxTileX();
    }

    /** Returns the index of the bottom row of tiles. */
    public int getMaxTileY(Long id) throws RemoteException {

        return getSource(id).getMaxTileY();
    }

    /** Returns the SampleModel associated with this image. */
    public SerializableState getSampleModel(Long id) throws RemoteException {
        return SerializerFactory.getState(getSource(id).getSampleModel(), null);
    }

    /** Returns the ColorModel associated with this image. */
    public SerializableState getColorModel(Long id) throws RemoteException {
        return SerializerFactory.getState(getSource(id).getColorModel(), null);
    }

    /** Returns a Rectangle indicating the image bounds. */
    public Rectangle getBounds(Long id) throws RemoteException {

        return getSource(id).getBounds();
    }

    /**
     * Returns tile (x, y). Note that x and y are indices into the tile array, not pixel locations. Unlike in the true
     * RenderedImage interface, the Raster that is returned should be considered a copy.
     *
     * @param id An ID for the source which must be unique across all clients.
     * @param tileX the X index of the requested tile in the tile array.
     * @param tileY the Y index of the requested tile in the tile array.
     * @return the tile as a Raster.
     */
    public SerializableState getTile(Long id, int tileX, int tileY) throws RemoteException {

        Raster r = getSource(id).getTile(tileX, tileY);
        return SerializerFactory.getState(r, null);
    }

    /**
     * Compresses tile (x, y) and returns the compressed tile's contents as a byte array. Note that x and y are indices
     * into the tile array, not pixel locations. Unlike in the true RenderedImage interface, the Raster that is returned
     * should be considered a copy.
     *
     * @param id An ID for the source which must be unique across all clients.
     * @param x the x index of the requested tile in the tile array
     * @param y the y index of the requested tile in the tile array
     * @return a byte array containing the compressed tile contents.
     */
    public byte[] getCompressedTile(Long id, int x, int y) throws RemoteException {

        TileCodecParameterList tcpl = null;
        TileEncoderFactory tef = null;
        NegotiableCapability codecCap = null;

        if (negotiated != null) {
            codecCap = ((NegotiableCapabilitySet) negotiated.get(id)).getNegotiatedValue("tileCodec");
        }

        if (codecCap != null) {

            String category = codecCap.getCategory();
            String capabilityName = codecCap.getCapabilityName();
            List generators = codecCap.getGenerators();

            Class factory;
            for (Iterator i = generators.iterator(); i.hasNext(); ) {

                factory = (Class) i.next();
                if (tef == null && TileEncoderFactory.class.isAssignableFrom(factory)) {
                    try {
                        tef = (TileEncoderFactory) factory.newInstance();
                    } catch (InstantiationException ie) {
                        throw new RuntimeException(ie.getMessage());
                    } catch (IllegalAccessException iae) {
                        throw new RuntimeException(iae.getMessage());
                    }
                }
            }

            if (tef == null) {
                throw new RuntimeException(JaiI18N.getString("JAIRMIImageServer0"));
            }

            TileCodecDescriptor tcd = (TileCodecDescriptor)
                    JAI.getDefaultInstance().getOperationRegistry().getDescriptor("tileEncoder", capabilityName);

            if (tcd.includesSampleModelInfo() == false || tcd.includesLocationInfo() == false) {
                throw new RuntimeException(JaiI18N.getString("JAIRMIImageServer1"));
            }

            ParameterListDescriptor pld = tcd.getParameterListDescriptor("tileEncoder");

            tcpl = new TileCodecParameterList(capabilityName, new String[] {"tileEncoder"}, pld);

            if (pld != null) {

                String paramNames[] = pld.getParamNames();
                String currParam;
                Object currValue;

                if (paramNames != null) {
                    for (int i = 0; i < paramNames.length; i++) {
                        currParam = paramNames[i];
                        try {
                            currValue = codecCap.getNegotiatedValue(currParam);
                        } catch (IllegalArgumentException iae) {
                            continue;
                        }
                        tcpl.setParameter(currParam, currValue);
                    }
                }
            }

            Raster r = getSource(id).getTile(x, y);
            ByteArrayOutputStream stream = new ByteArrayOutputStream();
            TileEncoder encoder = tef.createEncoder(stream, tcpl, r.getSampleModel());

            try {
                encoder.encode(r);
            } catch (java.io.IOException ioe) {
                throw new RuntimeException(ioe.getMessage());
            }

            return stream.toByteArray();
        } else {
            throw new RuntimeException(JaiI18N.getString("JAIRMIImageServer2"));
        }
    }

    /**
     * Returns the entire image as a single Raster.
     *
     * @return a Raster containing a copy of this image's data.
     */
    public SerializableState getData(Long id) throws RemoteException {
        return SerializerFactory.getState(getSource(id).getData(), null);
    }

    /**
     * Returns an arbitrary rectangular region of the RenderedImage in a Raster. The rectangle of interest will be
     * clipped against the image bounds.
     *
     * @param id An ID for the source which must be unique across all clients.
     * @param rect the region of the RenderedImage to be returned.
     * @return a SerializableState containing a copy of the desired data.
     */
    public SerializableState getData(Long id, Rectangle bounds) throws RemoteException {
        if (bounds == null) {
            return getData(id);
        } else {
            bounds = bounds.intersection(getBounds(id));
            return SerializerFactory.getState(getSource(id).getData(bounds), null);
        }
    }

    /** Returns the same result as getData(Rectangle) would for the same rectangular region. */
    public SerializableState copyData(Long id, Rectangle bounds) throws RemoteException {
        return getData(id, bounds);
    }

    /**
     * Creates a RenderedOp on the server side with a parameter block empty of sources. The sources are set by separate
     * calls depending upon the type and serializabilty of the source.
     */
    public void createRenderedOp(Long id, String opName, ParameterBlock pb, SerializableState hints)
            throws RemoteException {

        RenderingHints rh = (RenderingHints) hints.getObject();

        // Check whether any of the parameters are Strings which represent
        // images either on this server or another server.
        JAIRMIUtil.checkServerParameters(pb, nodes);

        RenderedOp node = new RenderedOp(opName, pb, rh);

        // Remove all sinks so that no events are sent automatically
        // to the sinks
        node.removeSinks();

        nodes.put(id, node);
    }

    /** Calls for Rendering of the Op and returns true if the RenderedOp could be rendered else false */
    public boolean getRendering(Long id) throws RemoteException {

        RenderedOp op = getNode(id);
        if (op.getRendering() == null) {
            return false;
        } else {
            return true;
        }
    }

    /** Retrieve a node from the hashtable */
    public RenderedOp getNode(Long id) throws RemoteException {
        return (RenderedOp) nodes.get(id);
    }

    /** Sets the source of the image as a RenderedImage on the server side */
    public synchronized void setRenderedSource(Long id, RenderedImage source, int index) throws RemoteException {

        PlanarImage pi = PlanarImage.wrapRenderedImage(source);

        Object obj = nodes.get(id);

        if (obj instanceof RenderedOp) {
            RenderedOp op = (RenderedOp) obj;
            op.setSource(pi, index);
            ((PlanarImage) op.getSourceObject(index)).removeSinks();
        } else if (obj instanceof RenderableOp) {
            ((RenderableOp) obj).setSource(pi, index);
        }
    }

    /** Sets the source of the image as a RenderedOp on the server side */
    public synchronized void setRenderedSource(Long id, RenderedOp source, int index) throws RemoteException {

        Object obj = nodes.get(id);
        if (obj instanceof RenderedOp) {
            RenderedOp op = (RenderedOp) obj;
            op.setSource(source.getRendering(), index);
            ((PlanarImage) op.getSourceObject(index)).removeSinks();
        } else if (obj instanceof RenderableOp) {
            ((RenderableOp) obj).setSource(source.getRendering(), index);
        }
    }

    /** Sets the source of the image which is on the same server */
    public synchronized void setRenderedSource(Long id, Long sourceId, int index) throws RemoteException {

        Object obj = nodes.get(id);
        if (obj instanceof RenderedOp) {
            RenderedOp op = (RenderedOp) obj;
            op.setSource(nodes.get(sourceId), index);
            ((PlanarImage) nodes.get(sourceId)).removeSinks();
        } else if (obj instanceof RenderableOp) {
            ((RenderableOp) obj).setSource(nodes.get(sourceId), index);
        }
    }

    /** Sets the source of the image which is on a different server */
    public synchronized void setRenderedSource(Long id, Long sourceId, String serverName, String opName, int index)
            throws RemoteException {

        Object obj = nodes.get(id);

        if (obj instanceof RenderedOp) {
            RenderedOp node = (RenderedOp) obj;
            node.setSource(new RMIServerProxy((serverName + "::" + sourceId), opName, null), index);
            ((PlanarImage) node.getSourceObject(index)).removeSinks();
        } else if (obj instanceof RenderableOp) {
            ((RenderableOp) obj).setSource(new RMIServerProxy((serverName + "::" + sourceId), opName, null), index);
        }
    }

    /// Renderable Mode Methods

    /**
     * Gets the minimum X coordinate of the rendering-independent image stored against the given ID.
     *
     * @return the minimum X coordinate of the rendering-independent image data.
     */
    public float getRenderableMinX(Long id) throws RemoteException {

        RenderableImage ri = (RenderableImage) nodes.get(id);
        return ri.getMinX();
    }

    /**
     * Gets the minimum Y coordinate of the rendering-independent image stored against the given ID.
     *
     * @return the minimum X coordinate of the rendering-independent image data.
     */
    public float getRenderableMinY(Long id) throws RemoteException {
        RenderableImage ri = (RenderableImage) nodes.get(id);
        return ri.getMinY();
    }

    /**
     * Gets the width (in user coordinate space) of the <code>RenderableImage</code> stored against the given ID.
     *
     * @return the width of the renderable image in user coordinates.
     */
    public float getRenderableWidth(Long id) throws RemoteException {
        RenderableImage ri = (RenderableImage) nodes.get(id);
        return ri.getWidth();
    }

    /**
     * Gets the height (in user coordinate space) of the <code>RenderableImage</code> stored against the given ID.
     *
     * @return the height of the renderable image in user coordinates.
     */
    public float getRenderableHeight(Long id) throws RemoteException {
        RenderableImage ri = (RenderableImage) nodes.get(id);
        return ri.getHeight();
    }

    /**
     * Creates a RenderedImage instance of this image with width w, and height h in pixels. The RenderContext is built
     * automatically with an appropriate usr2dev transform and an area of interest of the full image. All the rendering
     * hints come from hints passed in.
     *
     * <p>If w == 0, it will be taken to equal Math.round(h*(getWidth()/getHeight())). Similarly, if h == 0, it will be
     * taken to equal Math.round(w*(getHeight()/getWidth())). One of w or h must be non-zero or else an
     * IllegalArgumentException will be thrown.
     *
     * <p>The created RenderedImage may have a property identified by the String HINTS_OBSERVED to indicate which
     * RenderingHints were used to create the image. In addition any RenderedImages that are obtained via the
     * getSources() method on the created RenderedImage may have such a property.
     *
     * @param w the width of rendered image in pixels, or 0.
     * @param h the height of rendered image in pixels, or 0.
     * @param hints a RenderingHints object containg hints.
     * @return a RenderedImage containing the rendered data.
     */
    public RenderedImage createScaledRendering(Long id, int w, int h, SerializableState hintsState)
            throws RemoteException {

        RenderableImage ri = (RenderableImage) nodes.get(id);
        RenderingHints hints = (RenderingHints) hintsState.getObject();
        RenderedImage rendering = ri.createScaledRendering(w, h, hints);
        if (rendering instanceof Serializable) {
            return rendering;
        } else {
            return new SerializableRenderedImage(rendering);
        }
    }

    /**
     * Returnd a RenderedImage instance of this image with a default width and height in pixels. The RenderContext is
     * built automatically with an appropriate usr2dev transform and an area of interest of the full image. The
     * rendering hints are empty. createDefaultRendering may make use of a stored rendering for speed.
     *
     * @return a RenderedImage containing the rendered data.
     */
    public RenderedImage createDefaultRendering(Long id) throws RemoteException {

        RenderableImage ri = (RenderableImage) nodes.get(id);
        RenderedImage rendering = ri.createDefaultRendering();
        if (rendering instanceof Serializable) {
            return rendering;
        } else {
            return new SerializableRenderedImage(rendering);
        }
    }

    /**
     * Creates a RenderedImage that represented a rendering of this image using a given RenderContext. This is the most
     * general way to obtain a rendering of a RenderableImage.
     *
     * <p>The created RenderedImage may have a property identified by the String HINTS_OBSERVED to indicate which
     * RenderingHints (from the RenderContext) were used to create the image. In addition any RenderedImages that are
     * obtained via the getSources() method on the created RenderedImage may have such a property.
     *
     * @param renderContext the RenderContext to use to produce the rendering.
     * @return a RenderedImage containing the rendered data.
     */
    public RenderedImage createRendering(Long id, SerializableState renderContextState) throws RemoteException {

        RenderableImage ri = (RenderableImage) nodes.get(id);
        RenderContext renderContext = (RenderContext) renderContextState.getObject();
        RenderedImage rendering = ri.createRendering(renderContext);
        if (rendering instanceof Serializable) {
            return rendering;
        } else {
            return new SerializableRenderedImage(rendering);
        }
    }

    /**
     * Creates a RenderableOp on the server side with a parameter block empty of sources. The sources are set by
     * separate calls depending upon the type and serializabilty of the source.
     */
    public synchronized void createRenderableOp(Long id, String opName, ParameterBlock pb) throws RemoteException {

        // XXX Since RMIServerProxy does not do a checkClientParameters, this
        // side obviously does not do the corresponding
        // checkServerParameters. Look at RMIServerProxy's renderable
        // constructor for reasoning. aastha, 09/26/01

        RenderableOp node = new RenderableOp(opName, pb);
        nodes.put(id, node);
    }

    /** Calls for rendering of a RenderableOp with the given SerializableState */
    public synchronized Long getRendering(Long id, SerializableState rcs) throws RemoteException {

        RenderableOp op = (RenderableOp) nodes.get(id);
        PlanarImage pi = PlanarImage.wrapRenderedImage(op.createRendering((RenderContext) rcs.getObject()));

        Long renderingID = getRemoteID();
        nodes.put(renderingID, pi);

        // Put the op's negotiated result values for its rendering too.
        setServerNegotiatedValues(renderingID, (NegotiableCapabilitySet) negotiated.get(id));
        return renderingID;
    }

    /** Sets the source of the image which is on the same server */
    public synchronized void setRenderableSource(Long id, Long sourceId, int index) throws RemoteException {

        RenderableOp node = (RenderableOp) nodes.get(id);
        Object obj = nodes.get(sourceId);
        if (obj instanceof RenderableOp) {
            node.setSource((RenderableOp) obj, index);
        } else if (obj instanceof RenderedImage) {
            node.setSource(PlanarImage.wrapRenderedImage((RenderedImage) obj), index);
        }
    }

    /** Sets the source of the image which is on a different server */
    public synchronized void setRenderableSource(Long id, Long sourceId, String serverName, String opName, int index)
            throws RemoteException {

        RenderableOp node = (RenderableOp) nodes.get(id);
        node.setSource(new RMIServerProxy((serverName + "::" + sourceId), opName, null), index);
    }

    /** Sets the source of the image which is on a different server */
    public synchronized void setRenderableRMIServerProxyAsSource(
            Long id, Long sourceId, String serverName, String opName, int index) throws RemoteException {

        RenderableOp node = (RenderableOp) nodes.get(id);
        node.setSource(new RenderableRMIServerProxy(serverName, opName, null, sourceId), index);
    }

    /**
     * when source is set to a RenderableOp and isnt supposed to be rendered yet. like at the time of getBounds2D
     *
     * <p>Sets the source of the image as a RenderableOp on the server side
     */
    public synchronized void setRenderableSource(Long id, RenderableOp source, int index) throws RemoteException {
        RenderableOp op = (RenderableOp) nodes.get(id);
        op.setSource(source, index);
    }

    /** Sets the source of the image as a RenderableImage on the server side */
    public synchronized void setRenderableSource(Long id, SerializableRenderableImage s, int index)
            throws RemoteException {
        RenderableOp op = (RenderableOp) nodes.get(id);
        op.setSource(s, index);
    }

    /** Sets the source of the image as a RenderedImage on the server side */
    public synchronized void setRenderableSource(Long id, RenderedImage source, int index) throws RemoteException {

        PlanarImage pi = PlanarImage.wrapRenderedImage(source);
        RenderableOp op = (RenderableOp) nodes.get(id);
        op.setSource(pi, index);
    }

    /** Maps the RenderContext for the remote Image */
    public SerializableState mapRenderContext(int id, Long nodeId, String operationName, SerializableState rcs)
            throws RemoteException {

        // Retrieve the RenderableOp for the rendering of which
        // the mapRenderContext call is being made.
        RenderableOp rop = (RenderableOp) nodes.get(nodeId);

        // Find the CRIF for the respective operation
        ContextualRenderedImageFactory crif = CRIFRegistry.get(rop.getRegistry(), operationName);

        if (crif == null) {
            throw new RuntimeException(JaiI18N.getString("JAIRMIImageServer3"));
        }

        RenderContext rc = crif.mapRenderContext(
                id, (RenderContext) rcs.getObject(), (ParameterBlock) rop.getParameterBlock(), rop);
        return SerializerFactory.getState(rc, null);
    }

    /** Gets the Bounds2D of the specified Remote Image */
    public SerializableState getBounds2D(Long nodeId, String operationName) throws RemoteException {

        // Retrieve the RenderableOp for whose RIF
        // the mapRenderContext call is being made.
        RenderableOp rop = (RenderableOp) nodes.get(nodeId);

        // Find the CRIF for the respective operation
        ContextualRenderedImageFactory crif = CRIFRegistry.get(rop.getRegistry(), operationName);

        if (crif == null) {
            throw new RuntimeException(JaiI18N.getString("JAIRMIImageServer3"));
        }

        Rectangle2D r2D = crif.getBounds2D((ParameterBlock) rop.getParameterBlock());

        return SerializerFactory.getState(r2D, null);
    }

    /**
     * Returns <code>true</code> if successive renderings with the same arguments may produce different results for this
     * opName
     *
     * @return <code>false</code> indicating that the rendering is static.
     */
    public boolean isDynamic(String opName) throws RemoteException {

        return (CRIFRegistry.get(null, opName)).isDynamic();
    }

    /**
     * Returns <code>true</code> if successive renderings with the same arguments may produce different results for the
     * node represented by the given id.
     */
    public boolean isDynamic(Long id) throws RemoteException {

        RenderableImage node = (RenderableImage) nodes.get(id);
        return node.isDynamic();
    }

    /** Gets the operation names supported on the Server */
    public String[] getServerSupportedOperationNames() throws RemoteException {
        return JAI.getDefaultInstance().getOperationRegistry().getDescriptorNames(OperationDescriptor.class);
    }

    /** Gets the <code>OperationDescriptor</code>s of the operations supported on this server. */
    public List getOperationDescriptors() throws RemoteException {
        return JAI.getDefaultInstance().getOperationRegistry().getDescriptors(OperationDescriptor.class);
    }

    /**
     * Calculates the region over which two distinct renderings of an operation may be expected to differ.
     *
     * <p>The class of the returned object will vary as a function of the nature of the operation. For rendered and
     * renderable two- dimensional images this should be an instance of a class which implements <code>java.awt.Shape
     * </code>.
     *
     * @return The region over which the data of two renderings of this operation may be expected to be invalid or
     *     <code>null</code> if there is no common region of validity.
     */
    public synchronized SerializableState getInvalidRegion(
            Long id,
            ParameterBlock oldParamBlock,
            SerializableState oldRHints,
            ParameterBlock newParamBlock,
            SerializableState newRHints)
            throws RemoteException {

        RenderingHints oldHints = (RenderingHints) oldRHints.getObject();
        RenderingHints newHints = (RenderingHints) newRHints.getObject();

        RenderedOp op = (RenderedOp) nodes.get(id);

        OperationDescriptor od = (OperationDescriptor)
                JAI.getDefaultInstance().getOperationRegistry().getDescriptor("rendered", op.getOperationName());

        boolean samePBs = false;
        if (oldParamBlock == newParamBlock) samePBs = true;

        Vector oldSources = oldParamBlock.getSources();
        oldParamBlock.removeSources();
        Vector oldReplacedSources =
                JAIRMIUtil.replaceIdWithSources(oldSources, nodes, op.getOperationName(), op.getRenderingHints());
        oldParamBlock.setSources(oldReplacedSources);

        if (samePBs) {
            newParamBlock = oldParamBlock;
        } else {
            Vector newSources = newParamBlock.getSources();
            newParamBlock.removeSources();
            Vector newReplacedSources =
                    JAIRMIUtil.replaceIdWithSources(newSources, nodes, op.getOperationName(), op.getRenderingHints());

            newParamBlock.setSources(newReplacedSources);
        }

        Object invalidRegion = od.getInvalidRegion("rendered", oldParamBlock, oldHints, newParamBlock, newHints, op);

        SerializableState shapeState = SerializerFactory.getState((Shape) invalidRegion, null);

        return shapeState;
    }

    /**
     * Returns a conservative estimate of the destination region that can potentially be affected by the pixels of a
     * rectangle of a given source.
     *
     * @param id A <code>Long</code> identifying the node for whom the destination region needs to be calculated .
     * @param sourceRect The <code>Rectangle</code> in source coordinates.
     * @param sourceIndex The index of the source image.
     * @return A <code>Rectangle</code> indicating the potentially affected destination region, or <code>null</code> if
     *     the region is unknown.
     */
    public Rectangle mapSourceRect(Long id, Rectangle sourceRect, int sourceIndex) throws RemoteException {

        RenderedOp op = (RenderedOp) nodes.get(id);
        OpImage rendering = (OpImage) (op.getRendering());
        return rendering.mapSourceRect(sourceRect, sourceIndex);
    }

    /**
     * Returns a conservative estimate of the region of a specified source that is required in order to compute the
     * pixels of a given destination rectangle.
     *
     * @param id A <code>Long</code> identifying the node for whom the source region needs to be calculated .
     * @param destRect The <code>Rectangle</code> in destination coordinates.
     * @param sourceIndex The index of the source image.
     * @return A <code>Rectangle</code> indicating the required source region.
     */
    public Rectangle mapDestRect(Long id, Rectangle destRect, int sourceIndex) throws RemoteException {

        RenderedOp op = (RenderedOp) nodes.get(id);
        OpImage rendering = (OpImage) (op.getRendering());
        return rendering.mapDestRect(destRect, sourceIndex);
    }

    /** A method that handles the given event. */
    public synchronized Long handleEvent(Long renderedOpID, String propName, Object oldValue, Object newValue)
            throws RemoteException {

        RenderedOp op = (RenderedOp) nodes.get(renderedOpID);
        PlanarImage rendering = op.getRendering();

        // Get a new unique ID
        Long id = getRemoteID();
        // Cache the old rendering against the new id
        nodes.put(id, rendering);

        // Put the op's negotiated result values for its rendering too.
        setServerNegotiatedValues(id, (NegotiableCapabilitySet) negotiated.get(renderedOpID));

        // A PropertyChangeEventJAI with name "operationregistry",
        // "protocolname", "protocolandservername" or "servername" should
        // never be received here, since it is handled entirely on the
        // client side, so we don't handle those here.

        if (propName.equals("operationname")) {

            op.setOperationName((String) newValue);

        } else if (propName.equals("parameterblock")) {

            ParameterBlock newPB = (ParameterBlock) newValue;
            Vector newSrcs = newPB.getSources();
            newPB.removeSources();

            JAIRMIUtil.checkServerParameters(newPB, nodes);

            Vector replacedSources =
                    JAIRMIUtil.replaceIdWithSources(newSrcs, nodes, op.getOperationName(), op.getRenderingHints());
            newPB.setSources(replacedSources);

            op.setParameterBlock(newPB);

            // Remove the newly created sinks of the srcs in the newPB
            Vector newSources = newPB.getSources();
            if (newSources != null && newSources.size() > 0) {
                Iterator it = newSources.iterator();
                while (it.hasNext()) {
                    Object src = it.next();
                    if (src instanceof PlanarImage) {
                        ((PlanarImage) src).removeSinks();
                    } else if (src instanceof CollectionImage) {
                        ((CollectionImage) src).removeSinks();
                    }
                }
            }

        } else if (propName.equals("sources")) {

            Vector replacedSources = JAIRMIUtil.replaceIdWithSources(
                    (Vector) newValue, nodes, op.getOperationName(), op.getRenderingHints());
            op.setSources(replacedSources);

            // Remove the newly created sinks for the replacedSources
            if (replacedSources != null && replacedSources.size() > 0) {
                Iterator it = replacedSources.iterator();
                while (it.hasNext()) {
                    Object src = it.next();
                    if (src instanceof PlanarImage) {
                        ((PlanarImage) src).removeSinks();
                    } else if (src instanceof CollectionImage) {
                        ((CollectionImage) src).removeSinks();
                    }
                }
            }

        } else if (propName.equals("parameters")) {

            Vector parameters = (Vector) newValue;
            JAIRMIUtil.checkServerParameters(parameters, nodes);
            op.setParameters(parameters);

        } else if (propName.equals("renderinghints")) {

            SerializableState newState = (SerializableState) newValue;
            op.setRenderingHints((RenderingHints) newState.getObject());
        }

        return id;
    }

    /**
     * A method that handles a change in one of it's source's rendering, i.e. a change that would be signalled by
     * RenderingChangeEvent.
     */
    public synchronized Long handleEvent(
            Long renderedOpID, int srcIndex, SerializableState srcInvalidRegion, Object oldRendering)
            throws RemoteException {

        RenderedOp op = (RenderedOp) nodes.get(renderedOpID);
        PlanarImage rendering = op.getRendering();

        // Get a new unique ID
        Long id = getRemoteID();
        // Cache the old rendering against the new id
        nodes.put(id, rendering);

        // Put the op's negotiated result values for its rendering too.
        setServerNegotiatedValues(id, (NegotiableCapabilitySet) negotiated.get(renderedOpID));

        PlanarImage oldSrcRendering = null, newSrcRendering = null;
        String serverNodeDesc = null;
        Object src = null;

        if (oldRendering instanceof String) {

            serverNodeDesc = (String) oldRendering;
            int index = serverNodeDesc.indexOf("::");
            boolean diffServer = index != -1;

            if (diffServer) {
                // Create an RMIServerProxy to access the node on a
                // different server
                oldSrcRendering = new RMIServerProxy(serverNodeDesc, op.getOperationName(), op.getRenderingHints());
            } else {

                src = nodes.get(Long.valueOf(serverNodeDesc));

                if (src instanceof RenderedOp) {
                    oldSrcRendering = ((RenderedOp) src).getRendering();
                } else {
                    oldSrcRendering = PlanarImage.wrapRenderedImage((RenderedImage) src);
                }
            }

        } else {
            oldSrcRendering = PlanarImage.wrapRenderedImage((RenderedImage) oldRendering);
        }

        Object srcObj = op.getSource(srcIndex);
        if (srcObj instanceof RenderedOp) {
            newSrcRendering = ((RenderedOp) srcObj).getRendering();
        } else if (srcObj instanceof RenderedImage) {
            newSrcRendering = PlanarImage.wrapRenderedImage((RenderedImage) srcObj);
        }

        Shape invalidRegion = (Shape) srcInvalidRegion.getObject();

        RenderingChangeEvent rcEvent = new RenderingChangeEvent(
                (RenderedOp) op.getSource(srcIndex), oldSrcRendering, newSrcRendering, invalidRegion);
        op.propertyChange(rcEvent);

        return id;
    }

    /**
     * Returns the server's capabilities. Currently the only capabilities that are reported are those dealing with
     * TileCodecs.
     */
    public synchronized NegotiableCapabilitySet getServerCapabilities() {

        OperationRegistry registry = JAI.getDefaultInstance().getOperationRegistry();

        // Note that only the tileEncoder capabilities are returned from
        // this method since there is no way to distinguish between NC's
        // for the encoder and the decoder.
        String modeName = "tileEncoder";
        String[] descriptorNames = registry.getDescriptorNames(modeName);
        TileEncoderFactory tef = null;

        // Only non-preference NC's can be added.
        NegotiableCapabilitySet capabilities = new NegotiableCapabilitySet(false);

        Iterator it;
        for (int i = 0; i < descriptorNames.length; i++) {

            it = registry.getFactoryIterator(modeName, descriptorNames[i]);
            for (; it.hasNext(); ) {
                tef = (TileEncoderFactory) it.next();
                capabilities.add(tef.getEncodeCapability());
            }
        }

        return capabilities;
    }

    /**
     * Informs the server of the negotiated values that are the result of a successful negotiation.
     *
     * @param negotiatedValues The result of the negotiation.
     */
    public void setServerNegotiatedValues(Long id, NegotiableCapabilitySet negotiatedValues) throws RemoteException {
        if (negotiatedValues != null) negotiated.put(id, negotiatedValues);
        else negotiated.remove(id);
    }

    /**
     * Starts a server on a given port. The RMI registry must be running on the server host.
     *
     * <p>The usage of this class is
     *
     * <pre>
     * java -Djava.rmi.server.codebase=file:$JAI/lib/jai.jar \
     * -Djava.rmi.server.useCodebaseOnly=false \
     * -Djava.security.policy=\
     * file:`pwd`/policy org.eclipse.imagen.media.rmi.JAIRMIImageServer \
     * [-host hostName] [-port portNumber]
     * </pre>
     *
     * The default host is the local host and the default port is 1099.
     *
     * @param args the port number as a command-line argument.
     */
    public static void main(String[] args) {

        // Set the security manager.
        if (System.getSecurityManager() == null) {
            System.setSecurityManager(new RMISecurityManager());
        }

        // Load all JAIServerConfigurationSpi implementations on the CLASSPATH
        Iterator spiIter = Service.providers(JAIServerConfigurationSpi.class);
        JAI jai = JAI.getDefaultInstance();

        while (spiIter.hasNext()) {

            JAIServerConfigurationSpi serverSpi = (JAIServerConfigurationSpi) spiIter.next();
            serverSpi.updateServer(jai);
        }

        // Set the host name and port number.
        String host = null;
        int rmiRegistryPort = 1099; // default port is 1099
        int serverport = 0;

        if (args.length != 0) {

            String value;

            for (int i = 0; i < args.length; i++) {

                if (args[i].equalsIgnoreCase("-help")) {

                    System.out.println("Usage: java -Djava.rmi.server.codebase=file:$JAI/lib/jai.jar \\");
                    System.out.println("-Djava.rmi.server.useCodebaseOnly=false \\");
                    System.out.println("-Djava.security.policy=file:`pwd`/policy \\");
                    System.out.println("org.eclipse.imagen.media.rmi.JAIRMIImageServer \\");
                    System.out.println("\nwhere options are:");
                    System.out.println("\t-host <string> The server name or server IP address");
                    System.out.println("\t-port <integer> The port that rmiregistry is running on");
                    System.out.println("\t-rmiRegistryPort <integer> Same as -port option");
                    System.out.println(
                            "\t-serverPort <integer> The port that the server should listen on, for connections from clients");
                    System.out.println("\t-cacheMemCapacity <long> The memory capacity in bytes.");
                    System.out.println(
                            "\t-cacheMemThreshold <float> The memory threshold, which is the fractional amount of cache memory to retain during tile removal");
                    System.out.println(
                            "\t-disableDefaultCache Disable use of default tile cache. Tiles are not stored.");
                    System.out.println(
                            "\t-schedulerParallelism <integer> The degree of parallelism of the default TileScheduler");
                    System.out.println(
                            "\t-schedulerPrefetchParallelism <integer> The degree of parallelism of the default TileScheduler for tile prefetching");
                    System.out.println(
                            "\t-schedulerPriority <integer> The priority of tile scheduling for the default TileScheduler");
                    System.out.println(
                            "\t-schedulerPrefetchPriority <integer> The priority of tile prefetch scheduling for the default TileScheduler");
                    System.out.println(
                            "\t-defaultTileSize <integer>x<integer> The default tile dimensions in the form <xSize>x<ySize>");
                    System.out.println(
                            "\t-defaultRenderingSize <integer>x<integer> The default size to render a RenderableImage to, in the form <xSize>x<ySize>");
                    System.out.println(
                            "\t-serializeDeepCopy <boolean> Whether a deep copy of the image data should be used when serializing images");
                    System.out.println(
                            "\t-tileCodecFormat <string> The default format to be used for tile serialization via TileCodecs");
                    System.out.println(
                            "\t-retryInterval <integer> The retry interval value to be used for dealing with network errors during remote imaging");
                    System.out.println(
                            "\t-numRetries <integer> The number of retries to be used for dealing with network errors during remote imaging");

                } else if (args[i].equalsIgnoreCase("-host")) {

                    host = args[++i];

                } else if (args[i].equalsIgnoreCase("-port") || args[i].equalsIgnoreCase("-rmiRegistryPort")) {

                    rmiRegistryPort = Integer.parseInt(args[++i]);

                } else if (args[i].equalsIgnoreCase("-serverport")) {

                    serverport = Integer.parseInt(args[++i]);

                } else if (args[i].equalsIgnoreCase("-cacheMemCapacity")) {

                    jai.getTileCache().setMemoryCapacity(Long.parseLong(args[++i]));

                } else if (args[i].equalsIgnoreCase("-cacheMemThreshold")) {

                    jai.getTileCache().setMemoryThreshold(Float.parseFloat(args[++i]));

                } else if (args[i].equalsIgnoreCase("-disableDefaultCache")) {

                    jai.disableDefaultTileCache();

                } else if (args[i].equalsIgnoreCase("-schedulerParallelism")) {

                    jai.getTileScheduler().setParallelism(Integer.parseInt(args[++i]));

                } else if (args[i].equalsIgnoreCase("-schedulerPrefetchParallelism")) {

                    jai.getTileScheduler().setPrefetchParallelism(Integer.parseInt(args[++i]));

                } else if (args[i].equalsIgnoreCase("-schedulerPriority")) {

                    jai.getTileScheduler().setPriority(Integer.parseInt(args[++i]));

                } else if (args[i].equalsIgnoreCase("-schedulerPrefetchPriority")) {

                    jai.getTileScheduler().setPrefetchPriority(Integer.parseInt(args[++i]));

                } else if (args[i].equalsIgnoreCase("-defaultTileSize")) {

                    value = args[++i].toLowerCase();
                    int xpos = value.indexOf("x");
                    int xSize = Integer.parseInt(value.substring(0, xpos));
                    int ySize = Integer.parseInt(value.substring(xpos + 1));

                    jai.setDefaultTileSize(new Dimension(xSize, ySize));

                } else if (args[i].equalsIgnoreCase("-defaultRenderingSize")) {

                    value = args[++i].toLowerCase();
                    int xpos = value.indexOf("x");
                    int xSize = Integer.parseInt(value.substring(0, xpos));
                    int ySize = Integer.parseInt(value.substring(xpos + 1));

                    jai.setDefaultRenderingSize(new Dimension(xSize, ySize));

                } else if (args[i].equalsIgnoreCase("-serializeDeepCopy")) {

                    jai.setRenderingHint(JAI.KEY_SERIALIZE_DEEP_COPY, Boolean.valueOf(args[++i]));

                } else if (args[i].equalsIgnoreCase("-tileCodecFormat")) {

                    jai.setRenderingHint(TileCodecUtils.KEY_TILE_CODEC_FORMAT, args[++i]);

                } else if (args[i].equalsIgnoreCase("-retryInterval")) {

                    jai.setRenderingHint(RemoteJAI.KEY_RETRY_INTERVAL, Integer.valueOf(args[++i]));

                } else if (args[i].equalsIgnoreCase("-numRetries")) {

                    jai.setRenderingHint(RemoteJAI.KEY_NUM_RETRIES, Integer.valueOf(args[++i]));
                }
            }
        }

        // Default to the local host if the host was not specified.
        if (host == null) {
            try {
                host = InetAddress.getLocalHost().getHostAddress();
            } catch (java.net.UnknownHostException e) {
                String message = JaiI18N.getString("RMIImageImpl1");
                sendExceptionToListener(message, new RemoteImagingException(message, e));
                /*
                                System.err.println(JaiI18N.getString("RMIImageImpl1") +
                                                   e.getMessage());
                                e.printStackTrace();
                */
            }
        }

        System.out.println(JaiI18N.getString("RMIImageImpl3") + " " + host + ":" + rmiRegistryPort);

        try {
            JAIRMIImageServer im = new JAIRMIImageServer(serverport);
            String serverName =
                    new String("rmi://" + host + ":" + rmiRegistryPort + "/" + JAIRMIDescriptor.IMAGE_SERVER_BIND_NAME);
            System.out.println(JaiI18N.getString("RMIImageImpl4") + " \"" + serverName + "\".");
            Naming.rebind(serverName, im);
            System.out.println(JaiI18N.getString("RMIImageImpl5"));
        } catch (Exception e) {
            String message = JaiI18N.getString("RMIImageImpl1");
            sendExceptionToListener(message, new RemoteImagingException(message, e));
            /*
                        System.err.println(JaiI18N.getString("RMIImageImpl0") +
                                           e.getMessage());
                        e.printStackTrace();
            */
        }
    }

    private static void sendExceptionToListener(String message, Exception e) {
        ImagingListener listener = ImageUtil.getImagingListener((RenderingHints) null);
        listener.errorOccurred(message, new RemoteImagingException(message, e), JAIRMIImageServer.class, false);
    }
}
